package couponMetaService

import (
	"errors"
	"fmt"
	"strconv"
	"time"

	"wowCoupon.com/cache"
	"wowCoupon.com/engine/cacheMachine"
	"wowCoupon.com/engine/statusMachine"
	"wowCoupon.com/models"
	"wowCoupon.com/pkg/e"
	"wowCoupon.com/pkg/flow"
	"wowCoupon.com/pkg/tools"
	"wowCoupon.com/service/couponRecordService"
)

type CouponMeta struct {
	ID             int64
	BusinessId     int64
	GoodId         int64
	Type           int64
	Title          string
	Amount         int64
	RemainAmount   int64
	Comments       string
	IssueStartTime int64
	IssueEndTime   int64
	ValidStartTime int64
	ValidEndTime   int64
	Status         int64
	CreatedBy      string
	EditedBy       string

	UserId string
	//Used for mysql string conditions. Eg: []string {"status = ? and remain_amount != ?", "1", "0"}
	StringConditions []string
	//A user will be put into this list after he rob the coupon,
	//and the user can not rob the same coupon successfully again, only once
	UserIdList *flow.BloomFilter
}

//GetByID gets a coupon meta by id from DB. ToB
func (c *CouponMeta) GetByID() (*models.CouponMeta, error) {
	res, err := models.GetCouponMetaByID(c.ID)
	if err != nil {
		return nil, err
	}

	return res, nil
}

//GetByStringConditions gets coupon metas by customized string conditions from DB. ToB
func (c *CouponMeta) GetByStringConditions() ([]*models.CouponMeta, error) {
	res, err := models.GetCouponMetasByStringConditions(c.StringConditions)
	if err != nil {
		return nil, err
	}

	return res, nil
}

//Add adds new coupon meta. ToB
func (c *CouponMeta) Add() error {
	coupon := map[string]interface{}{
		"business_id":      c.BusinessId,
		"good_id":          c.GoodId,
		"type":             c.Type,
		"title":            c.Title,
		"amount":           c.Amount,
		"remain_amount":    c.RemainAmount,
		"comments":         c.Comments,
		"issue_start_time": c.IssueStartTime,
		"issue_end_time":   c.IssueEndTime,
		"valid_start_time": c.ValidStartTime,
		"valid_end_time":   c.ValidEndTime,
		"status":           c.Status,
		"created_by":       c.CreatedBy,
		"edited_by":        c.EditedBy,
	}

	// add coupon meta info into mysql
	id, err := models.AddCouponMeta(coupon)
	if err != nil {
		return err
	}

	// add coupon id and expire time to status machine
	if err := statusMachine.AddCouponValidity(id, c.ValidEndTime); err != nil {
		models.DeleteCouponMetaByID(id)
		return err
	}

	// add business id and coupon meta id into bloomfilters
	flow.BusinessBf.BfAdd(fmt.Sprint(c.BusinessId))
	flow.CouponBf.BfAdd(fmt.Sprint(id))

	// add coupon meta template info into Redis
	cacheMachine.CacheMachiner.PreLoadTemplate(
		id, c.GoodId, c.Type, c.Title, c.Comments, c.ValidStartTime, c.ValidEndTime, c.Status,
	)

	return nil
}

//To B, check specified meta is in Cache(Redis) or not, by using business_id and good_id
func (c *CouponMeta) GetTemplateForCustomer() (*models.CouponMeta, error) {
	// get coupon meta template info for customer from Redis
	cacheTemplate, err := cache.RdbTP.HGetAll(fmt.Sprintf("%d", c.ID))
	if cacheTemplate != nil && err == nil {
		template := map[string]interface{}{}

		for key, val := range cacheTemplate {
			template[key] = val
		}

		res := &models.CouponMeta{}
		tools.MapToStruct(template, res)
		return res, nil
	}

	// get coupon meta template info for customer from Mysql
	res, err := models.GetCouponMetaByID(c.ID)

	if err != nil {
		return nil, err
	}

	return res, err
}

func (c *CouponMeta) UpdateCouponMetaStatus(couponMeta *models.CouponMeta) error {
	if err := models.UpdateCouponMetaStatus(c.ID, c.Status); err != nil {
		return err
	}

	return nil
}

func (c *CouponMeta) UpdateCouponMetaRemainAmount() error {
	if err := models.UpdateCouponMetaRemainAmount(c.ID, c.Status, c.RemainAmount); err != nil {
		return err
	}

	return nil
}

//EditByID edits a coupon meta by id. ToB
func (c *CouponMeta) EditByID() error {
	if ok, err := c.editable(map[string]interface{}{"id": c.ID}); !ok {
		return err
	}

	data := tools.StructToMap(c)

	if err := models.UpdatesCouponMetaByID(c.ID, data); err != nil {
		return err
	}

	return nil
}

//To B, edits coupon meta by constraints
func (c *CouponMeta) EditByConstraints(constraints map[string]interface{}) error {
	if ok, err := c.editable(constraints); !ok {
		return err
	}

	data := tools.StructToMap(c)

	if err := models.UpdatesCouponMetaByConstraints(constraints, data); err != nil {
		return err
	}

	return nil
}

//To B, edits a single coupon meta
func (c *CouponMeta) Edit(couponMeta *models.CouponMeta) error {
	/*
		if ok := models.Editable(couponMeta); !ok {
			return errors.New("can not edit this coupon meta")
		}
	*/

	if err := models.UpdatesCouponMeta(couponMeta); err != nil {
		return err
	}

	return nil
}

//To B, edits a batch of coupon metas
func (c *CouponMeta) Edits(couponMetas []*models.CouponMeta) error {
	for _, couponMeta := range couponMetas {
		if err := c.Edit(couponMeta); err != nil {
			return err
		}
	}

	return nil
}

//To B, determine if the coupon meta is editable now
func (c *CouponMeta) editable(constraints map[string]interface{}) (bool, error) {
	editingCoupon, err := models.GetCouponMetaByConstraints(constraints)

	if err != nil {
		return false, err
	}

	if editingCoupon.Status == e.COUPON_STATUS_NOT_EXIST {
		return false, errors.New(e.GetMsg(e.ERROR_COUPON_META_NOT_EXIST))
	}

	if editingCoupon.Status == e.COUPON_STATUS_NOT_START {
		return true, nil
	}

	return false, errors.New(e.GetMsg(e.ERROR_EDIT_COUPON_META_FAIL_ALREADY_START))
}

//To B, delete coupon meta by id
func (c *CouponMeta) DeleteByID() error {
	if err := models.DeleteCouponMetaByID(c.ID); err != nil {
		return err
	}

	return nil
}

//To B, delete coupon meta(s) by constraints
func (c *CouponMeta) Delete(constraints map[string]interface{}) error {
	if err := models.DeleteCouponMetasByConstraints(constraints); err != nil {
		return err
	}

	return nil
}

//To B, check specified meta is in DB or not, by using id, business_id and good_id
func (c *CouponMeta) ExistInDB() (bool, error) {
	exist, err := models.ExistCouponMetaByConstraints(
		map[string]interface{}{
			"id":          c.ID,
			"business_id": c.BusinessId,
			"good_id":     c.GoodId,
		},
	)

	return exist, err
}

//To B, check specified meta is in Cache(Redis) or not, by using business_id and good_id
func (c *CouponMeta) ExistInCache() (bool, error) {
	var key string = fmt.Sprint(c.BusinessId) + fmt.Sprint(c.GoodId)

	_, err := cache.Rdb.Get(key)
	switch {
	case err == cache.Nil:
		return false, nil
	case err != nil:
		return false, err
	}

	return true, nil
}

//To B, check specified meta is in Cache(Redis) or not, by using business_id and good_id
func (c *CouponMeta) GetStockFromCache() (int64, error) {
	BusinessKey := tools.GenRedisKeyBusiness(c.BusinessId)
	HashKeyStock := tools.GenRedisHashKeyStock(c.GoodId, c.ID)

	stockStr, err := cache.Rdb.HGet(BusinessKey, HashKeyStock)

	if err != nil {
		return 0, err
	}

	stock, err := strconv.ParseInt(stockStr, 10, 64)

	if err != nil {
		return 0, err
	}

	return stock, nil
}

//To B, check specified meta is in Cache(Redis) or not, by using business_id and good_id
func (c *CouponMeta) DeleteStockInCache() (int64, error) {
	BusinessKey := tools.GenRedisKeyBusiness(c.BusinessId)
	HashKeyStock := tools.GenRedisHashKeyStock(c.GoodId, c.ID)
	HashKeyIssueStartTime := tools.GenRedisHashKeyIssueStartTime(c.GoodId, c.ID)
	HashKeyIssueEndTime := tools.GenRedisHashKeyIssueEndTime(c.GoodId, c.ID)

	res, err := cache.Rdb.HDel(BusinessKey, HashKeyStock, HashKeyIssueStartTime, HashKeyIssueEndTime)

	if err != nil {
		return 0, err
	}

	return res, nil
}

//To C, check if the meta seckill activity is started and in progress or not
func (c *CouponMeta) InProgress() (bool, error) {
	redisKeyBusiness := tools.GenRedisKeyBusiness(c.BusinessId)
	redisHashKeyIssueStartTime := tools.GenRedisHashKeyIssueStartTime(c.GoodId, c.ID)
	redisHashKeyIssueEndTime := tools.GenRedisHashKeyIssueEndTime(c.GoodId, c.ID)

	startTimeStr, err1 := cache.Rdb.HGet(redisKeyBusiness, redisHashKeyIssueStartTime)
	endTimeStr, err2 := cache.Rdb.HGet(redisKeyBusiness, redisHashKeyIssueEndTime)

	//数据还没有加载到redis中，说明抢购活动还没开始
	if startTimeStr == "" || endTimeStr == "" || err1 != nil || err2 != nil {
		return false, e.ErrSeckillNotStart
	}

	startTime, _ := strconv.ParseInt(startTimeStr, 10, 64)
	endTime, _ := strconv.ParseInt(endTimeStr, 10, 64)

	now := time.Now().Unix()
	if now >= startTime && now < endTime {
		return true, nil
	} else if now < startTime {
		return false, e.ErrSeckillNotStart
	} else {
		return false, e.ErrSeckillEnd
	}
}

func (c *CouponMeta) IsEnd() bool {
	redisKeyBusiness := tools.GenRedisKeyBusiness(c.BusinessId)
	redisHashKeyIssueEndTime := tools.GenRedisHashKeyIssueEndTime(c.GoodId, c.ID)

	ddlStr, err := cache.Rdb.HGet(redisKeyBusiness, redisHashKeyIssueEndTime)

	//数据还没有加载到redis中，说明抢购活动还没开始
	if ddlStr == "" || err != nil {
		return false
	}

	ddl, _ := strconv.ParseInt(ddlStr, 10, 64)

	if time.Now().UnixNano() >= ddl {
		return true
	} else {
		return false
	}
}

//Check related coupon meta parameters from client request
func (c *CouponMeta) checkParams() error {
	/*if c.UserId <= 0 {
		return e.ErrUserID
	}*/

	if c.BusinessId <= 0 {
		return e.ErrBusinessID
	}

	if c.GoodId <= 0 {
		return e.ErrGoodID
	}

	if c.ID <= 0 {
		return e.ErrMetaID
	}

	return nil
}

//To C, Crazy buy
func (c *CouponMeta) Rob() (*RobResult, error) {
	// 1. Check parameters
	if err := c.checkParams(); err != nil {
		return &RobResult{Status: RobInvalidParams}, err
	}

	// 2. Check if it allows current request by flow control token bucket,
	// it will try 5 times, in 1 time.Second. If allowed, go ahead; otherwise, just return false
	if ok := flow.TokenBucketRedis.AllowWithTimes(1); !ok {
		return &RobResult{Status: RobTooBusy}, e.ErrSeckillTooBusy
	}

	// 3. Rob the stock, decrement by 1
	robResult, err := c.rob()
	if robResult.Status != RobOK {
		return robResult, err
	}

	// 4. To-Do, sync coupon record to MQ(or just Mysql)
	couponRecordService := couponRecordService.CouponRecord{
		CouponMetaID: c.ID,
		UserId:       c.UserId,
		Status:       int64(couponRecordService.Ready),
	}

	// To-do, will change here in the future, will use rocketmq to cut peak for mysql
	if err := couponRecordService.Add(); err != nil {
		return &RobResult{Status: RobUnknownError}, err
	}

	return &RobResult{Status: RobOK}, nil
}

func (c *CouponMeta) rob() (*RobResult, error) {
	// 1. Check if BusinessID is in bloomfilter
	if ok, err := c.existBusiness(); !ok {
		return &RobResult{Status: RobBusinessNotExist}, err
	}

	// 2. Check if coupon meta ID is in bloomfilter
	if ok, err := c.existCoupon(); !ok {
		return &RobResult{Status: RobCouponNotExist}, err
	}

	// 3. Check if the coupon stock has been in Redis
	if robResult, err := c.hitStock(); robResult.Status != RobOK {
		// if data not in cache
		if robResult.Status == RobStockNotCached {
			// try to load from mysql
			if loadResult := c.loadStockFromMysqlToRedis(); loadResult != LoadSuccess {
				// load fail, retry cache
				return c.hitStock()
			}
			// load success, also rob success
			return &RobResult{Status: RobOK}, nil
		}

		return robResult, err
	}
	// Success
	return &RobResult{Status: RobOK}, nil
}

func (c *CouponMeta) existBusiness() (bool, error) {
	if ok := flow.BusinessBf.BfExists(fmt.Sprint(c.BusinessId)); !ok {
		return false, e.ErrInvalidBusinessId
	}

	return true, nil
}

func (c *CouponMeta) existCoupon() (bool, error) {
	if ok := flow.CouponBf.BfExists(fmt.Sprint(c.ID)); !ok {
		return false, e.ErrInvalidCouponMetaId
	}

	return true, nil
}

func (c *CouponMeta) hitStock() (*RobResult, error) {
	// Rob the stock, decrement by 1
	redisKeyBusiness := tools.GenRedisKeyBusiness(c.BusinessId)
	redisHashKeyStock := tools.GenRedisHashKeyStock(c.GoodId, c.ID)

	leftStock, err := cache.Rdb.HDecrBy(redisKeyBusiness, redisHashKeyStock, 1)

	if err != nil {
		if err == cache.Nil {
			return &RobResult{Status: RobStockNotCached}, err
		}
		return &RobResult{Status: RobUnknownError}, err
	}

	if leftStock >= 0 {
		// Check if the user has got the coupon before, a little false positives
		// If got before, return false
		/*
			if c.UserIdList.BfExists(fmt.Sprint(c.UserId)) {
				//give back stock，fail
				cache.Rdb.HIncrBy(redisKeyBusiness, redisHashKeyStock, 1)
				return &RobResult{Status: RobCouponEmpty}, e.ErrSeckillCouponEmpty
			}*/
		// Put user id into list before return success
		/*
			if ok := c.UserIdList.BfAdd(fmt.Sprint(c.UserId)); !ok {
				cache.Rdb.HIncrBy(redisKeyBusiness, redisHashKeyStock, 1)
				return &RobResult{Status: RobCouponEmpty}, e.ErrSeckillCouponEmpty
			}*/
		// Success
		return &RobResult{Status: RobOK}, nil

	} else {
		// Coupon stock has been emptied, fail
		cache.Rdb.HIncrBy(redisKeyBusiness, redisHashKeyStock, 1)
		return &RobResult{Status: RobCouponEmpty}, e.ErrSeckillCouponEmpty
	}

}

type LoadResult int

const (
	LoadSuccess LoadResult = iota
	LoadLockFail
	LoadDataFail
	LoadDataNotExist
	LoadCacheFail
	LoadRemainAmountZero
)

func (c *CouponMeta) loadStockFromMysqlToRedis() LoadResult {
	// Distributed lock, 30s expiration to prevent resource leakage
	getLock, err := cache.Rdb.SetNX("Lock", 1, 30*time.Second)
	if err != nil {
		time.Sleep(200 * time.Millisecond)
		return LoadLockFail
	}

	// Distributed unlock
	defer func() {
		cache.Rdb.Del("Lock")
	}()

	// If lock fail
	if !getLock {
		// Sleep 200 ms, then exit
		time.Sleep(200 * time.Millisecond)
		return LoadLockFail
	}

	// If lock success, try to load data from mysql to redis
	coupon, err := c.GetByID()
	if err != nil {
		return LoadDataFail
	}

	if coupon == nil {
		return LoadDataNotExist
	}

	// Try to load data to redis
	businessKey := tools.GenRedisKeyBusiness(coupon.BusinessId)
	stockKey := tools.GenRedisHashKeyStock(coupon.GoodId, int64(coupon.ID))
	remainAmount := coupon.RemainAmount
	var bingo bool

	if remainAmount > 0 {
		remainAmount -= 1
		bingo = true
	} else {
		bingo = false
	}

	ok, err := cache.Rdb.HSet(businessKey, stockKey, remainAmount)

	// Load fail
	if !ok || err != nil {
		return LoadCacheFail
	}

	// Load success but remain amount is already zero
	if bingo == false {
		return LoadRemainAmountZero
	}

	// Load success
	return LoadSuccess
}
